﻿using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

using RayDen.Library.Core;

namespace RayDen.Library.Data
{
    public partial class GenericExpressionEvaluator<TValue> : DynamicObject
    {
        private readonly Stack<Expression> expressionStack = new Stack<Expression>();
        private readonly Stack<char> operatorStack = new Stack<char>();
        private readonly List<string> parameters = new List<string>();

        private Func<object, TValue> converter;
        private Func<string, TValue> constantConverter;

        public GenericExpressionEvaluator(Func<object, TValue> converter, Func<string, TValue> constantConverter)
        {
            this.converter = converter;
            this.constantConverter = constantConverter;
        }


        public Func<object, TValue> Compile(string expression)
        {
            var compiled = Parse(expression);

            Func<object, TValue> result = argument =>
                {
                    var arguments = ParseArguments(argument);
                    return Execute(compiled, arguments);
                };

            return result;
        }

        public TValue Evaluate(string expression, object argument = null)
        {
            var arguments = ParseArguments(argument);

            return Evaluate(expression, arguments);
        }

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            if ("Evaluate" != binder.Name)
            {
                return base.TryInvokeMember(binder, args, out result);
            }

            if (!(args[0] is string))
            {
                throw new ArgumentException("No expression specified for parsing");
            }

            //args will contain expression and arguments,
            //ArgumentNames will contain only named arguments
            if (args.Length != binder.CallInfo.ArgumentNames.Count + 1)
            {
                throw new ArgumentException("Argument names missing.");
            }

            var arguments = new Dictionary<string, TValue>();

            for (int i = 0; i < binder.CallInfo.ArgumentNames.Count; i++)
            {
                if (IsNumeric(args[i + 1].GetType()))
                {
                    arguments.Add(binder.CallInfo.ArgumentNames[i], converter(args[i + 1]));
                }
            }

            result = Evaluate((string)args[0], arguments);

            return true;
        }


        private Func<TValue[], TValue> Parse(string expression)
        {
            if (string.IsNullOrWhiteSpace(expression))
            {
                return s => default(TValue);
            }

            var arrayParameter = Expression.Parameter(typeof(TValue[]), "args");

            parameters.Clear();
            operatorStack.Clear();
            expressionStack.Clear();

            var lambda = ParseExpression(expression, arrayParameter);
            var compiled = lambda.Compile();
            return compiled;
        }

        private Expression<Func<TValue[], TValue>> ParseExpression(string expression, ParameterExpression arrayParameter)
        {
            using (var reader = new StringReader(expression))
            {
                int peek;
                while ((peek = reader.Peek()) > -1)
                {
                    var next = (char)peek;

                    if (char.IsDigit(next) || next == '{')
                    {
                        expressionStack.Push(ReadOperand(reader));
                        continue;
                    }

                    if (next == '#')
                    {
                        expressionStack.Push(ReadVariable(reader, arrayParameter));
                        continue;
                    }

                    if (char.IsLetter(next))
                    {
                        expressionStack.Push(ReadParameter(reader, arrayParameter));
                        continue;
                    }

                    if (next == '$')
                    {
                        expressionStack.Push(ReadFunction(reader, arrayParameter));
                        continue;
                    }

                    if (Operation.IsDefined(next))
                    {
                        var currentOperation = ReadOperation(reader);

                        EvaluateWhile(() => operatorStack.Count > 0 && operatorStack.Peek() != '(' &&
                                            currentOperation.Precedence <= ((Operation)operatorStack.Peek()).Precedence);

                        operatorStack.Push(next);
                        continue;
                    }

                    if (next == '(')
                    {
                        reader.Read();
                        operatorStack.Push('(');
                        continue;
                    }

                    if (next == ')')
                    {
                        reader.Read();
                        EvaluateWhile(() => operatorStack.Count > 0 && operatorStack.Peek() != '(');
                        operatorStack.Pop();
                        continue;
                    }


                    if (next == '>')
                    {
                        reader.Read();
                        EvaluateWhile(() => operatorStack.Count > 0 && operatorStack.Peek() != '(');
                        continue;
                    }

                    if (next == ' ')
                    {
                        reader.Read();
                    }
                    else
                    {
                        throw new ArgumentException(string.Format("Encountered invalid character {0}", next),
                                                    "expression");
                    }
                }
            }

            EvaluateWhile(() => operatorStack.Count > 0);

            var lambda = Expression.Lambda<Func<TValue[], TValue>>(expressionStack.Pop(), arrayParameter);
            return lambda;
        }

        private Expression ReadVariable(TextReader reader, ParameterExpression arrayParameter)
        {
            var parameterName = reader.ReadWhile(c => c != '<').Trim(' ','#','<');
            this.parameters.Add(parameterName);
            return ReadExpression(reader, arrayParameter);
        }

        protected Expression ReadExpression(TextReader reader, Expression arrayParameter)
        {
            Stack<char> _operatorStack = new Stack<char>();
            Stack<Expression> _expressionStack = new Stack<Expression>();

            Expression arg = null;
            var next = (char)reader.Peek();

            while (next != ',' && next != '>')
            {
                if (next == '<')
                {
                    reader.Read();
                }
                else
                    if (char.IsDigit(next))
                    {
                        //arg = arg == null ? ReadOperand(reader) : Expression.AndAlso(arg, ReadOperand(reader));
                        arg = ReadOperand(reader);
                        _expressionStack.Push(arg);
                    }
                    else if (char.IsLetter(next))
                    {
                        arg = ReadParameter(reader, arrayParameter);
                        //arg == null ? ReadParameter(reader, arrayParameter) : Expression.AndAlso(arg, ReadParameter(reader, arrayParameter));
                        _expressionStack.Push(arg);
                    }
                    else
                        if (next == '(')
                        {
                            reader.Read();
                            _operatorStack.Push('(');
                        }
                        else

                            if (next == ')')
                            {
                                reader.Read();
                                EvaluateWhile(() => _operatorStack.Count > 0 && _operatorStack.Peek() != '(', _expressionStack, _operatorStack);
                                _operatorStack.Pop();
                            }
                            else if (next == '$')
                            {
                                arg = ReadFunction(reader, arrayParameter);
                                //arg == null ? ReadFunction(reader, arrayParameter) : Expression.AndAlso(arg, ReadFunction(reader, arrayParameter));
                                _expressionStack.Push(arg);
                            }
                            else if (Operation.IsDefined(next))
                            {
                                var currentOperation = ReadOperation(reader);

                                EvaluateWhile(() =>
                                {
                                    return _operatorStack.Count > 0 && _operatorStack.Peek() != '(' &&
                                           currentOperation.Precedence <= ((Operation)_operatorStack.Peek()).Precedence;
                                }, _expressionStack, _operatorStack);

                                _operatorStack.Push(next);
                            }
                next = (char)reader.Peek();
            }
            //EvaluateWhile(() => _operatorStack.Count > 0, _expressionStack, _operatorStack);
            reader.Read();

            return arg;
        }

        private Dictionary<string, TValue> ParseArguments(object argument)
        {
            if (argument == null)
            {
                return new Dictionary<string, TValue>();
            }

            var argumentType = argument.GetType();

            var properties = argumentType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                                         .Where(p => p.CanRead && (IsNumeric(p.PropertyType) || p.PropertyType == typeof(TValue)));

            var arguments = properties.ToDictionary(property => property.Name,
                                                    property => converter(property.GetValue(argument, null)));

            return arguments;
        }

        private TValue Evaluate(string expression, Dictionary<string, TValue> arguments)
        {
            var compiled = Parse(expression);

            return Execute(compiled, arguments);
        }

        private TValue Execute(Func<TValue[], TValue> compiled, Dictionary<string, TValue> arguments)
        {
            arguments = arguments ?? new Dictionary<string, TValue>();

            if (parameters.Count > arguments.Count)
            {
                throw new ArgumentException(string.Format("Expression contains {0} parameters but got only {1}",
                                                          parameters.Count, arguments.Count));
            }

            var missingParameters = parameters.Where(p => !arguments.ContainsKey(p)).ToList();

            if (missingParameters.Any())
            {
                throw new ArgumentException("No values provided for parameters: " + string.Join(",", missingParameters));
            }

            var values = parameters.Select(parameter => arguments[parameter]).ToArray();

            return compiled(values);
        }


        private void EvaluateWhile(Func<bool> condition, Stack<Expression> _expressionStack=null,Stack<char> _operationStack =null)
        {
            while (condition())
            {
                var right = (_expressionStack?? expressionStack).Pop();
                var left = (_expressionStack ?? expressionStack).Pop();

                (_expressionStack ?? expressionStack).Push(((Operation)(_operationStack ?? operatorStack).Pop()).Apply(left, right));
            }
        }

        private Expression ReadOperand(TextReader reader)
        {
            var operand = string.Empty;

            int peek;

            while ((peek = reader.Peek()) > -1)
            {
                var next = (char)peek;

                if (char.IsDigit(next) || next == '.')
                {
                    reader.Read();
                    operand += next;
                }
                else
                    if (next == '{')
                    {
                        operand = reader.ReadWhile(c => c != '}');
                        return Expression.Constant(constantConverter(operand));

                    }
                    else
                    {
                        break;
                    }
            }
            return Expression.Constant(double.Parse(operand));
        }

        private Operation ReadOperation(TextReader reader)
        {
            var operation = (char)reader.Read();
            return (Operation)operation;
        }

        private Expression ReadParameter(TextReader reader, Expression arrayParameter)
        {
            var parameter = string.Empty;

            int peek;

            while ((peek = reader.Peek()) > -1)
            {
                var next = (char)peek;

                // $n
                if (next == '$') //function call
                {
                    return ReadFunction(reader, arrayParameter);
                }
                else
                    if (char.IsLetter(next))
                    {
                        reader.Read();
                        parameter += next;
                    }
                    else
                    {
                        break;
                    }
            }

            if (!parameters.Contains(parameter))
            {
                parameters.Add(parameter);
            }


            return Expression.ArrayIndex(arrayParameter, Expression.Constant(parameters.IndexOf(parameter)));
        }

        private bool IsNumeric(Type type)
        {
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.SByte:
                case TypeCode.Byte:
                case TypeCode.Int16:
                case TypeCode.UInt16:
                case TypeCode.Int32:
                case TypeCode.UInt32:
                case TypeCode.Int64:
                case TypeCode.UInt64:
                case TypeCode.Single:
                case TypeCode.Double:
                case TypeCode.Decimal:
                    return true;
            }
            return false;
        }
    }
}